use std::{
    path::{Path, PathBuf},
    sync::Arc,
};

use anyhow::{Context, bail, ensure};
use derive_more::{Constructor, Debug, Deref, From};
use flexi_logger::{Age, Criterion, Duplicate, FileSpec, LoggerHandle, Naming, WriteMode};
use log::{debug, info, log_enabled, warn};
use once_cell::sync::OnceCell;
use strum::EnumString;

use crate::err::{AnyOk, AnyResult};

#[repr(C)]
#[derive(
    Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, EnumString, serde::Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Level {
    Error = 1,
    Warn,
    Info,
    Debug,
    Trace,
}

impl Default for Level {
    #[inline]
    fn default() -> Self {
        Self::Info
    }
}

impl From<Level> for log::Level {
    #[inline]
    fn from(value: Level) -> Self {
        match value {
            Level::Error => log::Level::Error,
            Level::Warn => log::Level::Warn,
            Level::Info => log::Level::Info,
            Level::Debug => log::Level::Debug,
            Level::Trace => log::Level::Trace,
        }
    }
}

#[derive(
    Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, EnumString, serde::Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Format {
    Plain,
    Detail,
}

impl Default for Format {
    #[inline]
    fn default() -> Self {
        Self::Plain
    }
}

#[derive(Debug, Clone, Default, serde::Deserialize)]
#[serde(default)]
pub struct Config {
    pub dir: Option<PathBuf>,
    pub level: Level,
    #[serde(rename = "format")]
    pub fmt: Format,
    pub write_to_file: bool,
    pub xdg_if_available: bool,
    pub direct_mode: bool,
}

#[repr(transparent)]
#[derive(Debug, Constructor, From, Deref)]
pub struct Logger(#[debug(skip)] LoggerHandle);

#[derive(Debug)]
pub struct LoggerBuilder {
    prefix: String,
    config: Config,
    #[debug(skip)]
    fx_logger: flexi_logger::Logger,
}

static SHARED_LOGGER: OnceCell<Arc<Logger>> = OnceCell::new();
static LOGGER_PREFIX: OnceCell<String> = OnceCell::new();

impl Logger {
    const PATH_ENV_KEY: &str = "Z_LOG_PATH";

    #[inline]
    pub fn exists() -> bool {
        SHARED_LOGGER.get().is_some()
    }

    #[inline]
    fn get_prefix() -> &'static str {
        LOGGER_PREFIX.get().map(|s| s.as_str()).unwrap_or("<rs>")
    }

    #[inline]
    pub fn flush() {
        SHARED_LOGGER.get().map(|v| v.flush()).unwrap_or_default()
    }

    #[inline]
    fn logs_path<P>(base: P) -> PathBuf
    where
        P: AsRef<Path>,
    {
        base.as_ref().join("logs")
    }

    #[inline]
    fn env_logs_path(key: &str) -> AnyResult<PathBuf> {
        std::env::var_os(key)
            .map(|v| {
                if log_enabled!(log::Level::Debug) {
                    eprintln!("detected env={key}, value={v:?}")
                }
                Self::logs_path(PathBuf::from(v))
            })
            .with_context(|| format!("env={key} not found"))
    }

    #[inline]
    fn xdg_logs_path(prefix: &str) -> AnyResult<PathBuf> {
        cross_xdg::BaseDirs::new()
            .map(|p| Self::logs_path(p.state_home().join(prefix.to_lowercase())))
            .context("unable to create base xdg")
    }

    #[inline]
    fn plain_format(
        w: &mut dyn std::io::Write,
        _: &mut flexi_logger::DeferredNow,
        r: &log::Record,
    ) -> Result<(), std::io::Error> {
        write!(w, "[{}] [{}] {}", Self::get_prefix(), r.level(), r.args())
    }

    #[inline]
    fn detail_format(
        w: &mut dyn std::io::Write,
        n: &mut flexi_logger::DeferredNow,
        r: &log::Record,
    ) -> Result<(), std::io::Error> {
        write!(
            w,
            "{} [{}] [{}] [{}] {}",
            n.now().format("%+"),
            Self::get_prefix(),
            std::thread::current().name().unwrap_or("<unnamed>"),
            r.level(),
            r.args()
        )
    }

    pub fn builder(config: Config, prefix: String) -> AnyResult<LoggerBuilder> {
        let fx_logger =
            flexi_logger::Logger::try_with_env_or_str(log::Level::from(config.level).to_string())
                .inspect_err(|e| eprintln!("[FATAL] [{prefix}] fx logger build failed: {e:?}"))
                .context("fx logger build failed")?
                .write_mode(if config.direct_mode {
                    WriteMode::Direct
                } else {
                    WriteMode::BufferAndFlush
                })
                .rotate(
                    Criterion::Age(Age::Day),
                    Naming::Timestamps,
                    flexi_logger::Cleanup::KeepLogAndCompressedFiles(7, 7),
                )
                .format(match config.fmt {
                    Format::Detail => Self::detail_format,
                    _ => Self::plain_format,
                })
                .duplicate_to_stdout(Duplicate::All)
                .duplicate_to_stderr(Duplicate::Warn)
                .use_utc();
        Ok(LoggerBuilder {
            prefix,
            config,
            fx_logger,
        })
    }
}

impl LoggerBuilder {
    pub fn start(self) -> AnyOk {
        let LoggerBuilder {
            prefix,
            mut config,
            mut fx_logger,
        } = self;

        LOGGER_PREFIX.get_or_init(|| prefix.clone());

        SHARED_LOGGER
            .get_or_try_init(|| {
                let mut use_default_dir = false;
                let mut dir_err = None;

                if config.write_to_file {
                    if config.dir.is_none() {
                        match Logger::env_logs_path(Logger::PATH_ENV_KEY).or_else(|e| {
                            if config.xdg_if_available {
                                Logger::xdg_logs_path(&prefix)
                            } else {
                                bail!("{e}, no available log-dir while 'write_to_file' is enabled")
                            }
                        }) {
                            Ok(p) => {
                                use_default_dir = true;
                                config.dir = Some(p)
                            }
                            Err(e) => dir_err = Some(e),
                        }
                    }

                    if dir_err.is_none() {
                        ensure!(config.dir.is_some(), "log-dir not set");
                        fx_logger = fx_logger.log_to_file(
                            FileSpec::default()
                                .directory(config.dir.as_ref().unwrap())
                                .basename(chrono::Local::now().format("%F").to_string()),
                        );
                    }
                }

                let logger = fx_logger
                    .start()
                    .map(Logger::new)
                    .with_context(|| format!("logger start failed, path={:?}", config.dir))?;

                if log_enabled!(log::Level::Debug) {
                    if let Some(e) = dir_err {
                        warn!("{e}")
                    }

                    if config.dir.is_some() {
                        if use_default_dir {
                            warn!("log-dir not set, but default log-dir is ENABLED")
                        }

                        debug!("logger locate to path={:?}", config.dir)
                    } else {
                        debug!("logger duplicate to file is DISABLED")
                    }
                }

                info!(
                    "logger start ok (spec={})",
                    logger.current_log_spec().unwrap_or_default()
                );

                logger.flush();

                Ok(logger.into())
            })
            .map(|_| ())
            .context("logger start failed")
    }
}
